/**
 * Copyright 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "mpriscontrolplugin-win.h"
#include "plugin_mpris_debug.h"

#include <core/device.h>

#include <KPluginFactory>

#include <QBuffer>

#include <chrono>
#include <random>

#include <ppltasks.h>

using namespace Windows::Foundation;

K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json")

MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args) : KdeConnectPlugin(parent, args) {
    sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get();
    sessionManager->SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs){
        this->updatePlayerList();
    });
    this->updatePlayerList();
}

std::optional<QString> MprisControlPlugin::getPlayerName(GlobalSystemMediaTransportControlsSession const& player) {
    auto entry = std::find(this->playerList.constBegin(), this->playerList.constEnd(), player);

    if(entry == this->playerList.constEnd()) {
        qCWarning(KDECONNECT_PLUGIN_MPRIS) << "PlaybackInfoChanged received for no longer tracked session" << player.SourceAppUserModelId().c_str();
        return std::nullopt;
    }

    return entry.key();
}

QString MprisControlPlugin::randomUrl() {
    const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    std::default_random_engine generator;
    std::uniform_int_distribution<int> distribution(0,VALID_CHARS.size() - 1);
    
    const int size = 10;
    QString fileUrl(size, QChar());
    for(int i = 0; i < size; i++) {
        fileUrl[i] = VALID_CHARS[distribution(generator)];
    }

    return QStringLiteral("file://") + fileUrl;
}

void MprisControlPlugin::sendMediaProperties(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player) {
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if(packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    auto mediaProperties = player.TryGetMediaPropertiesAsync().get();

    np.set(QStringLiteral("title"), QString::fromWCharArray(mediaProperties.Title().c_str()));
    np.set(QStringLiteral("artist"), QString::fromWCharArray(mediaProperties.Artist().c_str()));
    np.set(QStringLiteral("album"), QString::fromWCharArray(mediaProperties.AlbumTitle().c_str()));
    np.set(QStringLiteral("albumArtUrl"), randomUrl());
    np.set(QStringLiteral("nowPlaying"), mediaProperties.Artist().empty() ?  QString::fromWCharArray(mediaProperties.Title().c_str()) : (QString::fromWCharArray(mediaProperties.Artist().c_str()) + QStringLiteral(" - ") + QString::fromWCharArray(mediaProperties.Title().c_str())));
    
    np.set(QStringLiteral("url"), QString());
    sendTimelineProperties(np, player, true); // "length"

    if(packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::sendPlaybackInfo(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player) {
    
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if(packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));
    
    sendMediaProperties(np, player);

    auto playbackInfo = player.GetPlaybackInfo();
    auto playbackControls = playbackInfo.Controls();

    np.set(QStringLiteral("isPlaying"), playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
    np.set(QStringLiteral("canPause"), playbackControls.IsPauseEnabled());
    np.set(QStringLiteral("canPlay"), playbackControls.IsPlayEnabled());
    np.set(QStringLiteral("canGoNext"), playbackControls.IsNextEnabled());
    np.set(QStringLiteral("canGoPrevious"), playbackControls.IsPreviousEnabled());

    sendTimelineProperties(np, player);

    if(packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::sendTimelineProperties(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, bool lengthOnly) {
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if(packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    auto timelineProperties = player.GetTimelineProperties();

    if(!lengthOnly){
        np.set(QStringLiteral("canSeek"), timelineProperties.MinSeekTime() != timelineProperties.MaxSeekTime());
        np.set(QStringLiteral("pos"), std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.Position() - timelineProperties.StartTime()).count());
    }
    np.set(QStringLiteral("length"), std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.EndTime() - timelineProperties.StartTime()).count());

    if(packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::updatePlayerList() {
    playerList.clear();
    playbackInfoChangedHandlers.clear();
    mediaPropertiesChangedHandlers.clear();
    timelinePropertiesChangedHandlers.clear();

    auto sessions = sessionManager->GetSessions();
    playbackInfoChangedHandlers.resize(sessions.Size());
    mediaPropertiesChangedHandlers.resize(sessions.Size());
    timelinePropertiesChangedHandlers.resize(sessions.Size());

    for(uint32_t i = 0; i < sessions.Size(); i++) {
        const auto player = sessions.GetAt(i);

        QString name = QString::fromWCharArray(player.SourceAppUserModelId().c_str());
        QString uniqueName  = name;
        for (int i = 2; playerList.contains(uniqueName); ++i) {
            uniqueName = name + QStringLiteral(" [") + QString::number(i) + QStringLiteral("]");
        }

        playerList.insert(uniqueName, player);

        player.PlaybackInfoChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, PlaybackInfoChangedEventArgs args){
            if(auto name = getPlayerName(player))
                this->sendPlaybackInfo(name.value(), player);
        }).swap(playbackInfoChangedHandlers[i]);
        concurrency::create_task([this, player]{
            std::chrono::milliseconds timespan(50);
            std::this_thread::sleep_for(timespan);

            if(auto name = getPlayerName(player))
                this->sendPlaybackInfo(name.value(), player);
        });

        if(auto name = getPlayerName(player))
            sendPlaybackInfo(name.value(), player);

        player.MediaPropertiesChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, MediaPropertiesChangedEventArgs args){
            if(auto name = getPlayerName(player))
                this->sendMediaProperties(name.value(), player);
        }).swap(mediaPropertiesChangedHandlers[i]);
        concurrency::create_task([this, player]{
            std::chrono::milliseconds timespan(50);
            std::this_thread::sleep_for(timespan);

            if(auto name = getPlayerName(player))
                this->sendMediaProperties(name.value(), player);
        });

        player.TimelinePropertiesChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, TimelinePropertiesChangedEventArgs args){
            if(auto name = getPlayerName(player))
                this->sendTimelineProperties(name.value(), player);
        }).swap(timelinePropertiesChangedHandlers[i]);
        concurrency::create_task([this, player]{
            std::chrono::milliseconds timespan(50);
            std::this_thread::sleep_for(timespan);

            if(auto name = getPlayerName(player))
                this->sendTimelineProperties(name.value(), player);
        });
    }

    sendPlayerList();
}

void MprisControlPlugin::sendPlayerList() {
    NetworkPacket np(PACKET_TYPE_MPRIS);

    np.set(QStringLiteral("playerList"), playerList.keys());
    np.set(QStringLiteral("supportAlbumArtPayload"), false); // TODO: Sending albumArt doesn't work

    sendPacket(np);
}

bool MprisControlPlugin::sendAlbumArt(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, QString artUrl) {
    qWarning(KDECONNECT_PLUGIN_MPRIS) << "Sending Album Art";
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if(packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));
    
    auto thumbnail = player.TryGetMediaPropertiesAsync().get().Thumbnail();
    if(thumbnail) {
        auto stream = thumbnail.OpenReadAsync().get();
        if(stream && stream.CanRead()) {
            IBuffer data = Buffer(stream.Size());
            data = stream.ReadAsync(data, stream.Size(), InputStreamOptions::None).get();
            QSharedPointer<QBuffer> qdata = QSharedPointer<QBuffer>(new QBuffer());
            qdata->setData((char*)data.data(), data.Capacity());

            np.set(QStringLiteral("transferringAlbumArt"), true);
            np.set(QStringLiteral("albumArtUrl"), artUrl);

            np.setPayload(qdata, qdata->size());

            if(packetOrName.index() == 1)
                sendPacket(np);

            return true;
        }

        return false;
    }
    else {
        return false;
    }
}

bool MprisControlPlugin::receivePacket(const NetworkPacket &np)
{
    if (np.has(QStringLiteral("playerList"))) {
        return false; //Whoever sent this is an mpris client and not an mpris control!
    }

    //Send the player list
    const QString name = np.get<QString>(QStringLiteral("player"));
    auto it = playerList.find(name);
    bool valid_player = (it != playerList.end());
    if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
        sendPlayerList();
        if (!valid_player) {
            return true;
        }
    }

    auto player = it.value();

    if (np.has(QStringLiteral("albumArtUrl"))) {
        return sendAlbumArt(name, player, np.get<QString>(QStringLiteral("albumArtUrl")));
    }

    if (np.has(QStringLiteral("action"))) {
        const QString& action = np.get<QString>(QStringLiteral("action"));
        if(action == QStringLiteral("Next")) {
            player.TrySkipNextAsync().get();
        }
        else if(action == QStringLiteral("Previous")) {
            player.TrySkipPreviousAsync().get();
        }
        else if (action == QStringLiteral("Pause"))
        {
            player.TryPauseAsync().get();
        }
        else if (action == QStringLiteral("PlayPause"))
        {
            player.TryTogglePlayPauseAsync().get();
        }
        else if (action == QStringLiteral("Stop"))
        {
            player.TryStopAsync().get();
        }
        else if (action == QStringLiteral("Play"))
        {
            player.TryPlayAsync().get();
        }
    }
    if (np.has(QStringLiteral("setVolume"))) {
        qWarning(KDECONNECT_PLUGIN_MPRIS) << "Setting volume is not supported";
    }
    if (np.has(QStringLiteral("Seek"))) {
        TimeSpan offset = std::chrono::microseconds(np.get<int>(QStringLiteral("Seek")));
        qWarning(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset.count() << "ns to" << name;
        player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().Position() + offset).count()).get();
    }

    if (np.has(QStringLiteral("SetPosition"))){
        TimeSpan position = std::chrono::milliseconds(np.get<qlonglong>(QStringLiteral("SetPosition"), 0));
        player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get();
    }

    //Send something read from the mpris interface
    NetworkPacket answer(PACKET_TYPE_MPRIS);
    answer.set(QStringLiteral("player"), name);
    bool somethingToSend = false;
    if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
        sendPlaybackInfo(answer, player);
        somethingToSend = true;
    }
    if (np.get<bool>(QStringLiteral("requestVolume"))) {
        answer.set(QStringLiteral("volume"), 100);
        somethingToSend = true;
    }

    if (somethingToSend) {
        sendPacket(answer);
    }

    return true;
}

#include "mpriscontrolplugin-win.moc"
